/**
 * Copyright (c) 2010-2012, JGraph Ltd
 */
package com.mxgraph.shape;

import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.canvas.mxGraphicsCanvas2D;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxCellState;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Implements a stencil for the given XML definition. This class implements the mxGraph
 * stencil schema.
 */
public class mxStencil implements mxIShape {

    private static final Logger log = Logger.getLogger(mxStencil.class.getName());

    /**
     * Holds the top-level node of the stencil definition.
     */
    protected Element desc;

    /**
     * Holds the aspect of the shape. Default is "auto".
     */
    protected String aspect = null;

    /**
     * Holds the width of the shape. Default is 100.
     */
    protected double w0 = 100;

    /**
     * Holds the height of the shape. Default is 100.
     */
    protected double h0 = 100;

    /**
     * Holds the XML node with the stencil description.
     */
    protected Element bgNode = null;

    /**
     * Holds the XML node with the stencil description.
     */
    protected Element fgNode = null;

    /**
     * Holds the strokewidth direction from the description.
     */
    protected String strokewidth = null;

    /**
     * Holds the last x-position of the cursor.
     */
    protected double lastMoveX = 0;

    /**
     * Holds the last y-position of the cursor.
     */
    protected double lastMoveY = 0;

    /**
     * Constructs a new stencil for the given mxGraph shape description.
     *
     * @param description the description
     */
    public mxStencil(Element description) {
        setDescription(description);
    }

    /**
     * Returns the description.
     *
     * @return the description
     */
    public Element getDescription() {
        return desc;
    }

    /**
     * Sets the description.
     *
     * @param value the value
     */
    public void setDescription(Element value) {
        desc = value;
        parseDescription();
    }

    /**
     * Creates the canvas for rendering the stencil.
     *
     * @param gc the gc
     * @return the mx graphics canvas 2 d
     */
    protected mxGraphicsCanvas2D createCanvas(mxGraphics2DCanvas gc) {
        return new mxGraphicsCanvas2D(gc.getGraphics());
    }

    /**
     * Paints the stencil for the given state.
     */
    public void paintShape(mxGraphics2DCanvas gc, mxCellState state) {
        Map<String, Object> style = state.getStyle();
        mxGraphicsCanvas2D canvas = createCanvas(gc);

        double rotation = mxUtils.getDouble(style, mxConstants.STYLE_ROTATION,
                0);
        String direction = mxUtils.getString(style,
                mxConstants.STYLE_DIRECTION, null);

        // Default direction is east (ignored if rotation exists)
        if (direction != null) {
            if (direction.equals("north")) {
                rotation += 270;
            } else if (direction.equals("west")) {
                rotation += 180;
            } else if (direction.equals("south")) {
                rotation += 90;
            }
        }

        // New styles for shape flipping the stencil
        boolean flipH = mxUtils.isTrue(style, mxConstants.STYLE_STENCIL_FLIPH,
                false);
        boolean flipV = mxUtils.isTrue(style, mxConstants.STYLE_STENCIL_FLIPV,
                false);

        if (flipH && flipV) {
            rotation += 180;
            flipH = false;
            flipV = false;
        }

        // Saves the global state for each cell
        canvas.save();

        // Adds rotation and horizontal/vertical flipping
        rotation = rotation % 360;

        if (rotation != 0 || flipH || flipV) {
            canvas.rotate(rotation, flipH, flipV, state.getCenterX(),
                    state.getCenterY());
        }

        // Note: Overwritten in mxStencil.paintShape (can depend on aspect)
        mxRectangle aspect = computeAspect(state, state, direction);
        double minScale = Math.min(aspect.getWidth(), aspect.getHeight());
        double sw = strokewidth.equals("inherit") ? mxUtils.getDouble(
                state.getStyle(), mxConstants.STYLE_STROKEWIDTH, 1)
                * state.getView().getScale() : Double
                .parseDouble(strokewidth) * minScale;
        canvas.setStrokeWidth(sw);

        double alpha = mxUtils.getDouble(style, mxConstants.STYLE_OPACITY, 100) / 100;
        String gradientColor = mxUtils.getString(style,
                mxConstants.STYLE_GRADIENTCOLOR, null);

        // Converts colors with special keyword none to null
        if (gradientColor != null && gradientColor.equals(mxConstants.NONE)) {
            gradientColor = null;
        }

        String fillColor = mxUtils.getString(style,
                mxConstants.STYLE_FILLCOLOR, null);

        if (fillColor != null && fillColor.equals(mxConstants.NONE)) {
            fillColor = null;
        }

        String strokeColor = mxUtils.getString(style,
                mxConstants.STYLE_STROKECOLOR, null);

        if (strokeColor != null && strokeColor.equals(mxConstants.NONE)) {
            strokeColor = null;
        }

        // Draws the shadow if the fillColor is not transparent
        if (mxUtils.isTrue(style, mxConstants.STYLE_SHADOW, false)) {
            drawShadow(canvas, state, rotation, flipH, flipV, state, alpha, fillColor != null, aspect);
        }

        canvas.setAlpha(alpha);

        // Sets the dashed state
        if (mxUtils.isTrue(style, mxConstants.STYLE_DASHED, false)) {
            canvas.setDashed(true);
        }

        // Draws background and foreground
        if (strokeColor != null || fillColor != null) {
            if (strokeColor != null) {
                canvas.setStrokeColor(strokeColor);
            }

            if (fillColor != null) {
                if (gradientColor != null
                        && !gradientColor.equals("transparent")) {
                    canvas.setGradient(fillColor, gradientColor, state.getX(),
                            state.getY(), state.getWidth(), state.getHeight(),
                            direction, 1, 1);
                } else {
                    canvas.setFillColor(fillColor);
                }
            }

            // Draws background and foreground of shape
            drawShape(canvas, state, state, aspect, true);
            drawShape(canvas, state, state, aspect, false);
        }
    }

    /**
     * Draws the shadow.
     *
     * @param canvas   the canvas
     * @param state    the state
     * @param rotation the rotation
     * @param flipH    the flip h
     * @param flipV    the flip v
     * @param bounds   the bounds
     * @param alpha    the alpha
     * @param filled   the filled
     * @param aspect   the aspect
     */
    protected void drawShadow(mxGraphicsCanvas2D canvas, mxCellState state, double rotation, boolean flipH,
                              boolean flipV, mxRectangle bounds, double alpha, boolean filled, mxRectangle aspect) {
        // Requires background in generic shape for shadow, looks like only one
        // fillAndStroke is allowed per current path, try working around that
        // Computes rotated shadow offset
        double rad = rotation * Math.PI / 180;
        double cos = Math.cos(-rad);
        double sin = Math.sin(-rad);
        mxPoint offset = mxUtils.getRotatedPoint(new mxPoint(mxConstants.SHADOW_OFFSETX, mxConstants.SHADOW_OFFSETY), cos, sin);

        if (flipH) {
            offset.setX(offset.getX() * -1);
        }

        if (flipV) {
            offset.setY(offset.getY() * -1);
        }

        // TODO: Use save/restore instead of negative offset to restore (requires fix for HTML canvas)
        canvas.translate(offset.getX(), offset.getY());

        // Returns true if a shadow has been painted (path has been created)
        if (drawShape(canvas, state, bounds, aspect, true)) {
            canvas.setAlpha(mxConstants.STENCIL_SHADOW_OPACITY * alpha);
            // TODO: Implement new shadow
            //canvas.shadow(mxConstants.STENCIL_SHADOWCOLOR, filled);
        }

        canvas.translate(-offset.getX(), -offset.getY());
    }

    /**
     * Draws this stencil inside the given bounds.
     *
     * @param canvas     the canvas
     * @param state      the state
     * @param bounds     the bounds
     * @param aspect     the aspect
     * @param background the background
     * @return the boolean
     */
    public boolean drawShape(mxGraphicsCanvas2D canvas, mxCellState state,
                             mxRectangle bounds, mxRectangle aspect, boolean background) {
        Element elt = (background) ? bgNode : fgNode;

        if (elt != null) {
            lastMoveX = 0;
            lastMoveY = 0;

            Node tmp = elt.getFirstChild();

            while (tmp != null) {
                if (tmp.getNodeType() == Node.ELEMENT_NODE) {
                    drawElement(canvas, state, (Element) tmp, aspect);
                }

                tmp = tmp.getNextSibling();
            }

            return true;
        }

        return false;
    }

    /**
     * Returns a rectangle that contains the offset in x and y and the horizontal
     * and vertical scale in width and height used to draw this shape inside the
     * given rectangle.
     *
     * @param state     the state
     * @param bounds    the bounds
     * @param direction the direction
     * @return the mx rectangle
     */
    protected mxRectangle computeAspect(mxCellState state, mxRectangle bounds,
                                        String direction) {
        double x0 = bounds.getX();
        double y0 = bounds.getY();
        double sx = bounds.getWidth() / w0;
        double sy = bounds.getHeight() / h0;

        boolean inverse = (direction != null && (direction.equals("north") || direction
                .equals("south")));

        if (inverse) {
            sy = bounds.getWidth() / h0;
            sx = bounds.getHeight() / w0;

            double delta = (bounds.getWidth() - bounds.getHeight()) / 2;

            x0 += delta;
            y0 -= delta;
        }

        if (aspect.equals("fixed")) {
            sy = Math.min(sx, sy);
            sx = sy;

            // Centers the shape inside the available space
            if (inverse) {
                x0 += (bounds.getHeight() - this.w0 * sx) / 2;
                y0 += (bounds.getWidth() - this.h0 * sy) / 2;
            } else {
                x0 += (bounds.getWidth() - this.w0 * sx) / 2;
                y0 += (bounds.getHeight() - this.h0 * sy) / 2;
            }
        }

        return new mxRectangle(x0, y0, sx, sy);
    }

    /**
     * Drawsthe given element.
     *
     * @param canvas the canvas
     * @param state  the state
     * @param node   the node
     * @param aspect the aspect
     */
    protected void drawElement(mxGraphicsCanvas2D canvas, mxCellState state,
                               Element node, mxRectangle aspect) {
        String name = node.getNodeName();
        double x0 = aspect.getX();
        double y0 = aspect.getY();
        double sx = aspect.getWidth();
        double sy = aspect.getHeight();
        double minScale = Math.min(sx, sy);

        // LATER: Move to lookup table
        if (name.equals("save")) {
            canvas.save();
        } else if (name.equals("restore")) {
            canvas.restore();
        } else if (name.equals("path")) {
            canvas.begin();

            // Renders the elements inside the given path
            Node childNode = node.getFirstChild();

            while (childNode != null) {
                if (childNode.getNodeType() == Node.ELEMENT_NODE) {
                    drawElement(canvas, state, (Element) childNode, aspect);
                }

                childNode = childNode.getNextSibling();
            }
        } else if (name.equals("close")) {
            canvas.close();
        } else if (name.equals("move")) {
            lastMoveX = x0 + getDouble(node, "x") * sx;
            lastMoveY = y0 + getDouble(node, "y") * sy;
            canvas.moveTo(lastMoveX, lastMoveY);
        } else if (name.equals("line")) {
            lastMoveX = x0 + getDouble(node, "x") * sx;
            lastMoveY = y0 + getDouble(node, "y") * sy;
            canvas.lineTo(lastMoveX, lastMoveY);
        } else if (name.equals("quad")) {
            lastMoveX = x0 + getDouble(node, "x2") * sx;
            lastMoveY = y0 + getDouble(node, "y2") * sy;
            canvas.quadTo(x0 + getDouble(node, "x1") * sx,
                    y0 + getDouble(node, "y1") * sy, lastMoveX, lastMoveY);
        } else if (name.equals("curve")) {
            lastMoveX = x0 + getDouble(node, "x3") * sx;
            lastMoveY = y0 + getDouble(node, "y3") * sy;
            canvas.curveTo(x0 + getDouble(node, "x1") * sx,
                    y0 + getDouble(node, "y1") * sy, x0 + getDouble(node, "x2")
                            * sx, y0 + getDouble(node, "y2") * sy, lastMoveX,
                    lastMoveY);
        } else if (name.equals("arc")) {
            // Arc from stencil is turned into curves in image output
            double r1 = getDouble(node, "rx") * sx;
            double r2 = getDouble(node, "ry") * sy;
            double angle = getDouble(node, "x-axis-rotation");
            double largeArcFlag = getDouble(node, "large-arc-flag");
            double sweepFlag = getDouble(node, "sweep-flag");
            double x = x0 + getDouble(node, "x") * sx;
            double y = y0 + getDouble(node, "y") * sy;

            double[] curves = mxUtils.arcToCurves(this.lastMoveX,
                    this.lastMoveY, r1, r2, angle, largeArcFlag, sweepFlag, x,
                    y);

            for (int i = 0; i < curves.length; i += 6) {
                canvas.curveTo(curves[i], curves[i + 1], curves[i + 2],
                        curves[i + 3], curves[i + 4], curves[i + 5]);

                lastMoveX = curves[i + 4];
                lastMoveY = curves[i + 5];
            }
        } else if (name.equals("rect")) {
            canvas.rect(x0 + getDouble(node, "x") * sx,
                    y0 + getDouble(node, "y") * sy, getDouble(node, "w") * sx,
                    getDouble(node, "h") * sy);
        } else if (name.equals("roundrect")) {
            double arcsize = getDouble(node, "arcsize");

            if (arcsize == 0) {
                arcsize = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100;
            }

            double w = getDouble(node, "w") * sx;
            double h = getDouble(node, "h") * sy;
            double factor = arcsize / 100;
            double r = Math.min(w * factor, h * factor);

            canvas.roundrect(x0 + getDouble(node, "x") * sx,
                    y0 + getDouble(node, "y") * sy, getDouble(node, "w") * sx,
                    getDouble(node, "h") * sy, r, r);
        } else if (name.equals("ellipse")) {
            canvas.ellipse(x0 + getDouble(node, "x") * sx,
                    y0 + getDouble(node, "y") * sy, getDouble(node, "w") * sx,
                    getDouble(node, "h") * sy);
        } else if (name.equals("image")) {
            String src = evaluateAttribute(node, "src", state);

            canvas.image(x0 + getDouble(node, "x") * sx,
                    y0 + getDouble(node, "y") * sy, getDouble(node, "w") * sx,
                    getDouble(node, "h") * sy, src, false,
                    getString(node, "flipH", "0").equals("1"),
                    getString(node, "flipV", "0").equals("1"));
        } else if (name.equals("text")) {
            String str = evaluateAttribute(node, "str", state);
            double rotation = getString(node, "vertical", "0").equals("1") ? -90 : 0;

            canvas.text(x0 + getDouble(node, "x") * sx,
                    y0 + getDouble(node, "y") * sy, 0, 0, str,
                    node.getAttribute("align"), node.getAttribute("valign"),
                    false, "", null, false, rotation, null);
        } else if (name.equals("include-shape")) {
            mxStencil stencil = mxStencilRegistry.getStencil(node
                    .getAttribute("name"));

            if (stencil != null) {
                double x = x0 + getDouble(node, "x") * sx;
                double y = y0 + getDouble(node, "y") * sy;
                double w = getDouble(node, "w") * sx;
                double h = getDouble(node, "h") * sy;

                mxRectangle tmp = new mxRectangle(x, y, w, h);
                stencil.drawShape(canvas, state, tmp, aspect, true);
                stencil.drawShape(canvas, state, tmp, aspect, false);
            }
        } else if (name.equals("fillstroke")) {
            canvas.fillAndStroke();
        } else if (name.equals("fill")) {
            canvas.fill();
        } else if (name.equals("stroke")) {
            canvas.stroke();
        } else if (name.equals("strokewidth")) {
            double s = (getInt(node, "fixed", 0) == 1) ? 1 : minScale;
            canvas.setStrokeWidth(getDouble(node, "width") * s);
        } else if (name.equals("dashed")) {
            String dashed = node.getAttribute("dashed");

            if (dashed != null) {
                canvas.setDashed(dashed.equals("1"));
            }
        } else if (name.equals("dashpattern")) {
            String value = node.getAttribute("pattern");

            if (value != null) {
                String[] tmp = value.split(" ");
                StringBuffer pat = new StringBuffer();

                for (int i = 0; i < tmp.length; i++) {
                    if (tmp[i].length() > 0) {
                        pat.append(Double.parseDouble(tmp[i]) * minScale);
                        pat.append(" ");
                    }
                }

                value = pat.toString();
            }

            canvas.setDashPattern(value);
        } else if (name.equals("strokecolor")) {
            canvas.setStrokeColor(node.getAttribute("color"));
        } else if (name.equals("linecap")) {
            canvas.setLineCap(node.getAttribute("cap"));
        } else if (name.equals("linejoin")) {
            canvas.setLineJoin(node.getAttribute("join"));
        } else if (name.equals("miterlimit")) {
            canvas.setMiterLimit(getDouble(node, "limit"));
        } else if (name.equals("fillcolor")) {
            canvas.setFillColor(node.getAttribute("color"));
        } else if (name.equals("fontcolor")) {
            canvas.setFontColor(node.getAttribute("color"));
        } else if (name.equals("fontstyle")) {
            canvas.setFontStyle(getInt(node, "style", 0));
        } else if (name.equals("fontfamily")) {
            canvas.setFontFamily(node.getAttribute("family"));
        } else if (name.equals("fontsize")) {
            canvas.setFontSize(getDouble(node, "size") * minScale);
        }
    }

    /**
     * Returns the given attribute or the default value.
     *
     * @param elt          the elt
     * @param attribute    the attribute
     * @param defaultValue the default value
     * @return the int
     */
    protected int getInt(Element elt, String attribute, int defaultValue) {
        String value = elt.getAttribute(attribute);

        if (value != null && value.length() > 0) {
            try {
                defaultValue = (int) Math.floor(Float.parseFloat(value));
            } catch (NumberFormatException e) {
                log.log(Level.SEVERE, "Invalid value for attribute " + attribute + " in " + elt.getTagName(), e);
            }
        }

        return defaultValue;
    }

    /**
     * Returns the given attribute or 0.
     *
     * @param elt       the elt
     * @param attribute the attribute
     * @return the double
     */
    protected double getDouble(Element elt, String attribute) {
        return getDouble(elt, attribute, 0);
    }

    /**
     * Returns the given attribute or the default value.
     *
     * @param elt          the elt
     * @param attribute    the attribute
     * @param defaultValue the default value
     * @return the double
     */
    protected double getDouble(Element elt, String attribute,
                               double defaultValue) {
        String value = elt.getAttribute(attribute);

        if (value != null && value.length() > 0) {
            try {
                defaultValue = Double.parseDouble(value);
            } catch (NumberFormatException e) {
                log.log(Level.SEVERE, "Invalid value for attribute " + attribute + " in " + elt.getTagName(), e);
            }
        }

        return defaultValue;
    }

    /**
     * Returns the given attribute or the default value.
     *
     * @param elt          the elt
     * @param attribute    the attribute
     * @param defaultValue the default value
     * @return the string
     */
    protected String getString(Element elt, String attribute,
                               String defaultValue) {
        String value = elt.getAttribute(attribute);

        if (value != null && value.length() > 0) {
            defaultValue = value;
        }

        return defaultValue;
    }

    /**
     * Parses the description of this shape.
     */
    protected void parseDescription() {
        // LATER: Preprocess nodes for faster painting
        fgNode = (Element) desc.getElementsByTagName("foreground").item(0);
        bgNode = (Element) desc.getElementsByTagName("background").item(0);
        w0 = getDouble(desc, "w", w0);
        h0 = getDouble(desc, "h", h0);

        // Possible values for aspect are: variable and fixed where
        // variable means fill the available space and fixed means
        // use w0 and h0 to compute the aspect.
        aspect = getString(desc, "aspect", "variable");

        // Possible values for strokewidth are all numbers and "inherit"
        // where the inherit means take the value from the style (ie. the
        // user-defined stroke-width). Note that the strokewidth is scaled
        // by the minimum scaling that is used to draw the shape (sx, sy).
        strokewidth = getString(desc, "strokewidth", "1");
    }

    /**
     * Gets the attribute for the given name from the given node. If the attribute
     * does not exist then the text content of the node is evaluated and if it is
     * a function it is invoked with <state> as the only argument and the return
     * value is used as the attribute value to be returned.
     *
     * @param elt       the elt
     * @param attribute the attribute
     * @param state     the state
     * @return the string
     */
    public String evaluateAttribute(Element elt, String attribute,
                                    mxCellState state) {
        String result = elt.getAttribute(attribute);

        if (result == null) {
            // JS functions as text content are currently not supported in Java
        }

        return result;
    }

}
